// import { reactive, unref, computed } from "vue";
import { RouterLink } from "./RouterLink";
import { RouterView } from "./RouterView";
import { createRouterMatcher } from "./matcher";

/**
 * 创建一个路由实例
 * @param options 用户配置信息
 */
export function createRouter(options) {
  const matcher = createRouterMatcher(options.routes, options); // 拍平路由配置项
  console.log(matcher);
  // const parseQuery = options.parseQuery || originalParseQuery;
  // const stringifyQuery = options.stringifyQuery || originalStringifyQuery;
  const routerHistory = options.history; // 传入的 history 信息
  console.log(routerHistory);
  // const beforeGuards = useCallbacks();
  // const beforeResolveGuards = useCallbacks();
  // const afterGuards = useCallbacks();
  // const currentRoute = shallowRef(START_LOCATION_NORMALIZED);
  // let pendingLocation = START_LOCATION_NORMALIZED;
  //   const normalizeParams = applyToParams.bind(
  //     null,
  //     (paramValue) => "" + paramValue
  //   );
  // const encodeParams = applyToParams.bind(null, encodeParam);

  // function addRoute(parentOrRoute, route) {
  //   let parent;
  //   let record;
  //   if (isRouteName(parentOrRoute)) {
  //     parent = matcher.getRecordMatcher(parentOrRoute);
  //     record = route;
  //   } else {
  //     record = parentOrRoute;
  //   }
  //   return matcher.addRoute(record, parent);
  // }
  // function removeRoute(name) {
  //   const recordMatcher = matcher.getRecordMatcher(name);
  //   if (recordMatcher) {
  //     matcher.removeRoute(recordMatcher);
  //   }
  // }

  // function getRoutes() {
  //   return matcher.getRoutes().map((routeMatcher) => routeMatcher.record);
  // }
  // function hasRoute(name) {
  //   return !!matcher.getRecordMatcher(name);
  // }

  // function resolve(rawLocation, currentLocation) {
  //   currentLocation = assign({}, currentLocation || currentRoute.value);
  //   if (typeof rawLocation === "string") {
  //     const locationNormalized = parseURL(
  //       parseQuery,
  //       rawLocation,
  //       currentLocation.path
  //     );
  //     const matchedRoute = matcher.resolve(
  //       { path: locationNormalized.path },
  //       currentLocation
  //     );
  //     const href = routerHistory.createHref(locationNormalized.fullPath);

  //     return assign(locationNormalized, matchedRoute, {
  //       hash: decode(locationNormalized.hash),
  //       redirectedFrom: undefined,
  //       href,
  //     });
  //   }

  //   let matcherLocation;

  //   // path could be relative in object as well
  //   if ("path" in rawLocation) {
  //     matcherLocation = assign({}, rawLocation, {
  //       path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
  //     });
  //   } else {
  //     // remove any nullish param
  //     const targetParams = assign({}, rawLocation.params);
  //     for (const key in targetParams) {
  //       if (targetParams[key] == null) {
  //         delete targetParams[key];
  //       }
  //     }
  //     // pass encoded values to the matcher, so it can produce encoded path and fullPath
  //     matcherLocation = assign({}, rawLocation, {
  //       params: encodeParams(rawLocation.params),
  //     });
  //     // current location params are decoded, we need to encode them in case the
  //     // matcher merges the params
  //     currentLocation.params = encodeParams(currentLocation.params);
  //   }

  //   const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
  //   const hash = rawLocation.hash || "";

  //   // the matcher might have merged current location params, so
  //   // we need to run the decoding again

  //   // matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));

  //   const fullPath = stringifyURL(
  //     stringifyQuery,
  //     assign({}, rawLocation, {
  //       hash: encodeHash(hash),
  //       path: matchedRoute.path,
  //     })
  //   );

  //   const href = routerHistory.createHref(fullPath);

  //   return assign(
  //     {
  //       fullPath,
  //       // keep the hash encoded so fullPath is effectively path + encodedQuery +
  //       // hash
  //       hash,
  //       query:
  //         // if the user is using a custom query lib like qs, we might have
  //         // nested objects, so we keep the query as is, meaning it can contain
  //         // numbers at `$route.query`, but at the point, the user will have to
  //         // use their own type anyway.
  //         // https://github.com/vuejs/router/issues/328#issuecomment-649481567
  //         stringifyQuery === originalStringifyQuery
  //           ? normalizeQuery(rawLocation.query)
  //           : rawLocation.query || {},
  //     },
  //     matchedRoute,
  //     {
  //       redirectedFrom: undefined,
  //       href,
  //     }
  //   );
  // }

  // function locationAsObject(to) {
  //   return typeof to === "string"
  //     ? parseURL(parseQuery, to, currentRoute.value.path)
  //     : assign({}, to);
  // }

  // function checkCanceledNavigation(to, from) {
  //   if (pendingLocation !== to) {
  //     return (
  //       createRouterError <
  //       NavigationFailure >
  //       (ErrorTypes.NAVIGATION_CANCELLED,
  //       {
  //         from,
  //         to,
  //       })
  //     );
  //   }
  // }

  // function push(to) {
  //   return pushWithRedirect(to);
  // }

  // function replace(to) {
  //   return push(assign(locationAsObject(to), { replace: true }));
  // }

  // function handleRedirectRecord(to) {
  //   const lastMatched = to.matched[to.matched.length - 1];
  //   if (lastMatched && lastMatched.redirect) {
  //     const { redirect } = lastMatched;
  //     let newTargetLocation =
  //       typeof redirect === "function" ? redirect(to) : redirect;

  //     if (typeof newTargetLocation === "string") {
  //       newTargetLocation =
  //         newTargetLocation.includes("?") || newTargetLocation.includes("#")
  //           ? (newTargetLocation = locationAsObject(newTargetLocation))
  //           : // force empty params
  //             { path: newTargetLocation };

  //       newTargetLocation.params = {};
  //     }

  //     return assign(
  //       {
  //         query: to.query,
  //         hash: to.hash,
  //         // avoid transferring params if the redirect has a path
  //         params: "path" in newTargetLocation ? {} : to.params,
  //       },
  //       newTargetLocation
  //     );
  //   }
  // }

  // function pushWithRedirect(to, redirectedFrom) {
  //   const targetLocation = (pendingLocation = resolve(to));
  //   const from = currentRoute.value;
  //   const data = to.state;
  //   const force = to.force;
  //   const replace = to.replace === true;
  //   const shouldRedirect = handleRedirectRecord(targetLocation);

  //   if (shouldRedirect)
  //     return pushWithRedirect(
  //       assign(locationAsObject(shouldRedirect), {
  //         state:
  //           typeof shouldRedirect === "object"
  //             ? assign({}, data, shouldRedirect.state)
  //             : data,
  //         force,
  //         replace,
  //       }),
  //       // keep original redirectedFrom if it exists
  //       redirectedFrom || targetLocation
  //     );

  //   const toLocation = targetLocation;

  //   toLocation.redirectedFrom = redirectedFrom;
  //   let failure;

  //   if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
  //     failure =
  //       createRouterError <
  //       NavigationFailure >
  //       (ErrorTypes.NAVIGATION_DUPLICATED, { to: toLocation, from });
  //     handleScroll(
  //       from,
  //       from,

  //       true,

  //       false
  //     );
  //   }

  //   return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
  //     .catch((error) =>
  //       isNavigationFailure(error)
  //         ? // navigation redirects still mark the router as ready
  //           isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
  //           ? error
  //           : markAsReady(error) // also returns the error
  //         : // reject any unknown error
  //           triggerError(error, toLocation, from)
  //     )
  //     .then((failure) => {
  //       if (failure) {
  //         if (
  //           isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
  //         ) {
  //           return pushWithRedirect(
  //             // keep options
  //             assign(
  //               {
  //                 // preserve an existing replacement but allow the redirect to override it
  //                 replace,
  //               },
  //               locationAsObject(failure.to),
  //               {
  //                 state:
  //                   typeof failure.to === "object"
  //                     ? assign({}, data, failure.to.state)
  //                     : data,
  //                 force,
  //               }
  //             ),
  //             // preserve the original redirectedFrom if any
  //             redirectedFrom || toLocation
  //           );
  //         }
  //       } else {
  //         // if we fail we don't finalize the navigation
  //         failure = finalizeNavigation(toLocation, from, true, replace, data);
  //       }
  //       triggerAfterEach(toLocation, from, failure);
  //       return failure;
  //     });
  // }

  /**
   * Helper to reject and skip all navigation guards if a new navigation happened
   * @param to
   * @param from
   */
  // function checkCanceledNavigationAndReject(to, from) {
  //   const error = checkCanceledNavigation(to, from);
  //   return error ? Promise.reject(error) : Promise.resolve();
  // }

  // function navigate(to, from) {
  //   let guards;

  //   const [leavingRecords, updatingRecords, enteringRecords] =
  //     extractChangingRecords(to, from);

  //   // all components here have been resolved once because we are leaving
  //   guards = extractComponentsGuards(
  //     leavingRecords.reverse(),
  //     "beforeRouteLeave",
  //     to,
  //     from
  //   );

  //   // leavingRecords is already reversed
  //   for (const record of leavingRecords) {
  //     record.leaveGuards.forEach((guard) => {
  //       guards.push(guardToPromiseFn(guard, to, from));
  //     });
  //   }

  //   const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
  //     null,
  //     to,
  //     from
  //   );

  //   guards.push(canceledNavigationCheck);

  //   // run the queue of per route beforeRouteLeave guards
  //   return (
  //     runGuardQueue(guards)
  //       .then(() => {
  //         // check global guards beforeEach
  //         guards = [];
  //         for (const guard of beforeGuards.list()) {
  //           guards.push(guardToPromiseFn(guard, to, from));
  //         }
  //         guards.push(canceledNavigationCheck);

  //         return runGuardQueue(guards);
  //       })
  //       .then(() => {
  //         // check in components beforeRouteUpdate
  //         guards = extractComponentsGuards(
  //           updatingRecords,
  //           "beforeRouteUpdate",
  //           to,
  //           from
  //         );

  //         for (const record of updatingRecords) {
  //           record.updateGuards.forEach((guard) => {
  //             guards.push(guardToPromiseFn(guard, to, from));
  //           });
  //         }
  //         guards.push(canceledNavigationCheck);

  //         // run the queue of per route beforeEnter guards
  //         return runGuardQueue(guards);
  //       })
  //       .then(() => {
  //         // check the route beforeEnter
  //         guards = [];
  //         for (const record of to.matched) {
  //           // do not trigger beforeEnter on reused views
  //           if (record.beforeEnter && !from.matched.includes(record)) {
  //             if (isArray(record.beforeEnter)) {
  //               for (const beforeEnter of record.beforeEnter)
  //                 guards.push(guardToPromiseFn(beforeEnter, to, from));
  //             } else {
  //               guards.push(guardToPromiseFn(record.beforeEnter, to, from));
  //             }
  //           }
  //         }
  //         guards.push(canceledNavigationCheck);

  //         // run the queue of per route beforeEnter guards
  //         return runGuardQueue(guards);
  //       })
  //       .then(() => {
  //         // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>

  //         // clear existing enterCallbacks, these are added by extractComponentsGuards
  //         to.matched.forEach((record) => (record.enterCallbacks = {}));

  //         // check in-component beforeRouteEnter
  //         guards = extractComponentsGuards(
  //           enteringRecords,
  //           "beforeRouteEnter",
  //           to,
  //           from
  //         );
  //         guards.push(canceledNavigationCheck);

  //         // run the queue of per route beforeEnter guards
  //         return runGuardQueue(guards);
  //       })
  //       .then(() => {
  //         // check global guards beforeResolve
  //         guards = [];
  //         for (const guard of beforeResolveGuards.list()) {
  //           guards.push(guardToPromiseFn(guard, to, from));
  //         }
  //         guards.push(canceledNavigationCheck);

  //         return runGuardQueue(guards);
  //       })
  //       // catch any navigation canceled
  //       .catch((err) =>
  //         isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
  //           ? err
  //           : Promise.reject(err)
  //       )
  //   );
  // }

  // function triggerAfterEach(to, from, failure) {
  //   // navigation is confirmed, call afterGuards
  //   // TODO: wrap with error handlers
  //   for (const guard of afterGuards.list()) guard(to, from, failure);
  // }

  /**
   * - Cleans up any navigation guards
   * - Changes the url if necessary
   * - Calls the scrollBehavior
   */
  // function finalizeNavigation(toLocation, from, isPush, replace, data) {
  //   // a more recent navigation took place
  //   const error = checkCanceledNavigation(toLocation, from);
  //   if (error) return error;

  //   // only consider as push if it's not the first navigation
  //   const isFirstNavigation = from === START_LOCATION_NORMALIZED;
  //   const state = !isBrowser ? {} : history.state;

  //   // change URL only if the user did a push/replace and if it's not the initial navigation because
  //   // it's just reflecting the url
  //   if (isPush) {
  //     // on the initial navigation, we want to reuse the scroll position from
  //     // history state if it exists
  //     if (replace || isFirstNavigation)
  //       routerHistory.replace(
  //         toLocation.fullPath,
  //         assign(
  //           {
  //             scroll: isFirstNavigation && state && state.scroll,
  //           },
  //           data
  //         )
  //       );
  //     else routerHistory.push(toLocation.fullPath, data);
  //   }

  //   // accept current navigation
  //   currentRoute.value = toLocation;
  //   handleScroll(toLocation, from, isPush, isFirstNavigation);

  //   markAsReady();
  // }

  // let removeHistoryListener;
  // attach listener to history to trigger navigations
  // function setupListeners() {
  //   // avoid setting up listeners twice due to an invalid first navigation
  //   if (removeHistoryListener) return;
  //   removeHistoryListener = routerHistory.listen((to, _from, info) => {
  //     if (!router.listening) return;
  //     // cannot be a redirect route because it was in history
  //     const toLocation = resolve(to);

  //     // due to dynamic routing, and to hash history with manual navigation
  //     // (manually changing the url or calling history.hash = '#/somewhere'),
  //     // there could be a redirect record in history
  //     const shouldRedirect = handleRedirectRecord(toLocation);
  //     if (shouldRedirect) {
  //       pushWithRedirect(
  //         assign(shouldRedirect, { replace: true }),
  //         toLocation
  //       ).catch(noop);
  //       return;
  //     }

  //     pendingLocation = toLocation;
  //     const from = currentRoute.value;

  //     // TODO: should be moved to web history?
  //     if (isBrowser) {
  //       saveScrollPosition(
  //         getScrollKey(from.fullPath, info.delta),
  //         computeScrollPosition()
  //       );
  //     }

  //     navigate(toLocation, from)
  //       .catch((error) => {
  //         if (
  //           isNavigationFailure(
  //             error,
  //             ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
  //           )
  //         ) {
  //           return error;
  //         }
  //         if (
  //           isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
  //         ) {
  //           // Here we could call if (info.delta) routerHistory.go(-info.delta,
  //           // false) but this is bug prone as we have no way to wait the
  //           // navigation to be finished before calling pushWithRedirect. Using
  //           // a setTimeout of 16ms seems to work but there is no guarantee for
  //           // it to work on every browser. So instead we do not restore the
  //           // history entry and trigger a new navigation as requested by the
  //           // navigation guard.

  //           // the error is already handled by router.push we just want to avoid
  //           // logging the error
  //           pushWithRedirect(
  //             error.to,
  //             toLocation
  //             // avoid an uncaught rejection, let push call triggerError
  //           )
  //             .then((failure) => {
  //               // manual change in hash history #916 ending up in the URL not
  //               // changing, but it was changed by the manual url change, so we
  //               // need to manually change it ourselves
  //               if (
  //                 isNavigationFailure(
  //                   failure,
  //                   ErrorTypes.NAVIGATION_ABORTED |
  //                     ErrorTypes.NAVIGATION_DUPLICATED
  //                 ) &&
  //                 !info.delta &&
  //                 info.type === NavigationType.pop
  //               ) {
  //                 routerHistory.go(-1, false);
  //               }
  //             })
  //             .catch(noop);
  //           // avoid the then branch
  //           return Promise.reject();
  //         }
  //         // do not restore history on unknown direction
  //         if (info.delta) {
  //           routerHistory.go(-info.delta, false);
  //         }
  //         // unrecognized error, transfer to the global handler
  //         return triggerError(error, toLocation, from);
  //       })
  //       .then((failure) => {
  //         failure =
  //           failure ||
  //           finalizeNavigation(
  //             // after navigation, all matched components are resolved
  //             toLocation,
  //             from,
  //             false
  //           );

  //         // revert the navigation
  //         if (failure) {
  //           if (
  //             info.delta &&
  //             // a new navigation has been triggered, so we do not want to revert, that will change the current history
  //             // entry while a different route is displayed
  //             !isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED)
  //           ) {
  //             routerHistory.go(-info.delta, false);
  //           } else if (
  //             info.type === NavigationType.pop &&
  //             isNavigationFailure(
  //               failure,
  //               ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED
  //             )
  //           ) {
  //             // manual change in hash history #916
  //             // it's like a push but lacks the information of the direction
  //             routerHistory.go(-1, false);
  //           }
  //         }

  //         triggerAfterEach(toLocation, from, failure);
  //       })
  //       .catch(noop);
  //   });
  // }

  // Initialization and Errors

  // let readyHandlers = useCallbacks();
  // let errorHandlers = useCallbacks();
  // let ready;

  /**
   * Trigger errorHandlers added via onError and throws the error as well
   *
   * @param error - error to throw
   * @param to - location we were navigating to when the error happened
   * @param from - location we were navigating from when the error happened
   * @returns the error as a rejected promise
   */
  // function triggerError(error, to, from) {
  //   markAsReady(error);
  //   const list = errorHandlers.list();
  //   if (list.length) {
  //     list.forEach((handler) => handler(error, to, from));
  //   }
  //   return Promise.reject(error);
  // }

  // function isReady() {
  //   if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
  //     return Promise.resolve();
  //   return new Promise((resolve, reject) => {
  //     readyHandlers.add([resolve, reject]);
  //   });
  // }

  /**
   * Mark the router as ready, resolving the promised returned by isReady(). Can
   * only be called once, otherwise does nothing.
   * @param err - optional error
   */

  // function markAsReady(err) {
  //   if (!ready) {
  //     // still not ready if an error happened
  //     ready = !err;
  //     setupListeners();
  //     readyHandlers
  //       .list()
  //       .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
  //     readyHandlers.reset();
  //   }
  //   return err;
  // }

  // Scroll behavior
  // function handleScroll(to, from, isPush, isFirstNavigation) {
  //   const { scrollBehavior } = options;
  //   if (!isBrowser || !scrollBehavior) return Promise.resolve();

  //   const scrollPosition =
  //     (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
  //     ((isFirstNavigation || !isPush) &&
  //       history.state &&
  //       history.state.scroll) ||
  //     null;

  //   return nextTick()
  //     .then(() => scrollBehavior(to, from, scrollPosition))
  //     .then((position) => position && scrollToPosition(position))
  //     .catch((err) => triggerError(err, to, from));
  // }

  // const go = (delta) => routerHistory.go(delta);

  // let started;
  const installedApps = new Set();

  // 路由实例上的方法和属性
  const router = {
    // currentRoute,
    // listening: true,
    // addRoute,
    // removeRoute,
    // hasRoute,
    // getRoutes,
    // resolve,
    // options,
    // push,
    // replace,
    // go,
    // back: () => go(-1),
    // forward: () => go(1),
    // beforeEach: beforeGuards.add,
    // beforeResolve: beforeResolveGuards.add,
    // afterEach: afterGuards.add,
    // onError: errorHandlers.add,
    // isReady,

    // vue 插件安装必须要 install 方法
    install(app) {
      const router = this; // 路由实例
      // 注册全局组件
      app.component("RouterLink", RouterLink);
      app.component("RouterView", RouterView);
      // 全局上挂着路由实例
      app.config.globalProperties.$router = router;
      // Object.defineProperty(app.config.globalProperties, "$route", {
      //   enumerable: true,
      //   get: () => unref(currentRoute),
      // });

      // this initial navigation is only necessary on client, on server it doesn't
      // make sense because it will create an extra unnecessary navigation and could
      // lead to problems
      // if (
      //   isBrowser &&
      //   // used for the initial navigation client side to avoid pushing
      //   // multiple times when the router is used in multiple apps
      //   !started &&
      //   currentRoute.value === START_LOCATION_NORMALIZED
      // ) {
      //   // see above
      //   started = true;
      //   push(routerHistory.location).catch(() => {});
      // }

      // const reactiveRoute = {};

      // for (const key in START_LOCATION_NORMALIZED) {
      //   // @ts-expect-error: the key matches
      //   reactiveRoute[key] = computed(() => currentRoute.value[key]);
      // }

      // app.provide(routerKey, router);
      // app.provide(routeLocationKey, reactive(reactiveRoute));
      // app.provide(routerViewLocationKey, currentRoute);

      const unmountApp = app.unmount;
      installedApps.add(app);

      // 卸载时
      app.unmount = function () {
        installedApps.delete(app);
        // the router is not attached to an app anymore
        if (installedApps.size < 1) {
          // invalidate the current navigation
          // pendingLocation = START_LOCATION_NORMALIZED;
          // removeHistoryListener && removeHistoryListener();
          // removeHistoryListener = null;
          // currentRoute.value = START_LOCATION_NORMALIZED;
          // started = false;
          // ready = false;
        }
        unmountApp();
      };
    },
  };

  return router;
}
